package jude.java.money.batch.task;

import static jude.java.money.batch.Definition.CONFIG;
import static jude.java.money.batch.Definition.CONFIG_ABANDON;
import static jude.java.money.batch.Definition.CONFIG_CHECK_IP;
import static jude.java.money.batch.Definition.CONFIG_IGNORE;
import static jude.java.money.batch.Definition.CONFIG_READONLY;
import static jude.java.money.batch.Definition.CONFIG_SAME_CODE;
import static jude.java.money.batch.Definition.CONFIG_SAME_SERVER;
import static jude.java.money.batch.Definition.CONFIG_VPN;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jude.java.money.batch.Definition;
import jude.java.money.batch.Keys;
import jude.java.money.batch.Log;
import jude.java.money.batch.Main;
import jude.java.money.batch.Parameter;
import jude.java.money.batch.memory.IP;
import jude.java.money.batch.task.expand.Pretreatment;
import jude.java.money.batch.task.expand.ResultFilter;
import jude.java.money.batch.task.expand.UpdateCodeTask;
import jude.java.money.batch.task.expand.UpdateDatabaseTask;
import jude.java.util.Utils;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpMethodRetryHandler;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.DefaultHttpParams;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.lang.StringUtils;

public abstract class BatchTask<T> implements Runnable {
	
	protected static final String FILE_ENCODING = "UTF-8";
	protected static final String REQUEST_ENCODING = "UTF-8";
	
	/** 过滤站点. 如果设置了这个值, 则只处理这些网站 */
	public static String[] FILTER_SITES = null;
	protected static Log L;
	
	protected final Parameter<T> P;
	private boolean patchOnly;
	private Set<String> flags;
	private Set<String> filterSites;
	
	protected Pretreatment pretreatment;
	protected ResultFilter resultFilter;
	protected int groupFilter = -1;			//如果设置了, 则仅对些组有效
	protected String flagFilter;			//如果设置了, 仅对有些标志的组有效
	
	protected int totalCount;			//总连接 次数
	protected int errorCount;			//ConnectException 次数
	
	public BatchTask(Parameter<T> p) {
		this.P = p;
		initDefaultFlags();
		initSites();
		initFlags();
	}
	
	protected abstract String getLabel();
	
	protected void initFlags() {
	}
	
	private void initDefaultFlags() {
		this.flags = new HashSet<String>();
		flags.add(Definition.CONFIG_IGNORE);
		flags.add(Definition.CONFIG_ABANDON);
		flags.add(Definition.CONFIG_VPN);
		if (this instanceof UpdateCodeTask) {
			flags.add(Definition.CONFIG_SAME_CODE);
			flags.add(Definition.CONFIG_READONLY);
			flags.add(Definition.CONFIG_CHECK_IP);
		}
		if (this instanceof UpdateDatabaseTask) {
			flags.add(Definition.CONFIG_SAME_DB);
		}
	}
	
	private void initSites() {
		if (FILTER_SITES != null) {
			filterSites = new HashSet<String>();
			for (int i = 0; i < FILTER_SITES.length; i++) {
				filterSites.add(FILTER_SITES[i]);
			}
		}
	}
	
	protected void assertType(int type) {
		if (P.TYPE == 0 || P.TYPE == type)
			return;
		throw new RuntimeException("invalid type:" + P.TYPE);
	}
	
	public BatchTask<T> flags(String... is) {
		flags.clear();
		for (int i = 0; i < is.length; i++) {
			flags.add(is[i]);
		}
		return this;
	}
	
	public BatchTask<T> addFlags(String... is) {
		for (int i = 0; i < is.length; i++) {
			flags.add(is[i]);
		}
		return this;
	}
	
	public BatchTask<T> removeFlags(String... is) {
		for (int i = 0; i < is.length; i++) {
			flags.remove(is);
		}
		return this;
	}
	
	private boolean is(Map<String, String> configs, String... flags) {
		for (int i = 0; i < flags.length; i++) {
			String flag = flags[i];
			if (configs.containsKey(flag) && this.flags.contains(flag))
				return true;
		}
		return false;
	}
	
	/** 结果过滤 */
	public BatchTask<T> results(String... ignoredResults) {
		return set(ResultFilter.get(ignoredResults));
	}
	/** 结果过滤器 */
	public BatchTask<T> set(ResultFilter filter) {
		this.resultFilter = filter;
		return this;
	}
	/** 预处理器 */
	public BatchTask<T> set(Pretreatment pre) {
		this.pretreatment = pre;
		return this;
	}
	/** 设置分组过滤 */
	public BatchTask<T> groupFilter(int group) {
		this.groupFilter = group;
		return this;
	}
	/** 标记过滤 */
	public BatchTask<T> flagFilter(String flag) {
		this.flagFilter = flag;
		return this;
	}
	
	public void run() {
		L = getLog();
		String[][] sites = P.SITES;
		String path = P.PATH;
		if (path == null)
			path = "";
		else if (path.length() > 0 && !path.startsWith("/"))
			path = "/" + path;
		int n = 1;

		if (patchOnly) {
			createPatch();
		} else {
			List<String> failures = new ArrayList<String>();
			List<String> skips = new ArrayList<String>();
			for (int i = 0; !patchOnly && i < sites.length; i++) {
				int c = 0;
				boolean sameServer = false, skip = false;
				totalCount = errorCount = 0;
				Map<String, String> configs = new HashMap<String, String>();
				Set<String> ips = null;
				int group = -1;
				for (int j = 0; j < sites[i].length; j++) {
					String site = sites[i][j];
					if (j == 0 && handleConfigs(configs, site)) {
						if (groupFilter != -1) {
							group = Utils.getInteger(configs.get(Keys.GROUP), -1);
							if (group != groupFilter)
								break;
						}
						continue;
					}
					if (StringUtils.isNotEmpty(flagFilter)) {
						if (!configs.containsKey(flagFilter))
							break;
					}
					
					if (c == 0 && is(configs, CONFIG_ABANDON, CONFIG_IGNORE, CONFIG_VPN, CONFIG_READONLY))
						break;
					if (skip(site))
						continue;
					if (c == 0) {
						sameServer = configs.containsKey(CONFIG_SAME_CODE) || configs.containsKey(CONFIG_SAME_SERVER);
						if (is(configs, CONFIG_CHECK_IP)) {
							ips = new HashSet<String>();
						}
					} else if (is(configs, Definition.CONFIG_SAME_CODE, Definition.CONFIG_SAME_DB)) {
						skip = true;
					}
					if (ips != null) {
						String ip = IP.get(site);
						if (ip != null) {
							if (ips.contains(ip))
								continue;
							ips.add(ip);
						}
					}
					String url = "http://" + site + path;
					int index = n++;
					L.start("\r\n").id(getLabel() + index + "\t" + site);
					if (!sameServer)
						totalCount = errorCount = 0;
					L.skip(skip);
					try {
						if (!skip)
							execute(site, url, i, c);
						else
							skips.add(site);
					} catch (ConnectException e) {
						if (sameServer && errorCount >= 3)
							skip = true;
						L.result(e.getClass().getName() + " " + e.getMessage());
						failures.add(site);
					} catch (Throwable e) {
						L.result(e.getClass().getName() + " " + e.getMessage());
						e.printStackTrace();
						failures.add(site);
					}
					c ++;
					L.log("\r\n");
					L.end();
				}
				onFinish(i, sites.length);
			}
			writeStatistics("failures", failures);
			writeStatistics("skips", skips);
		}
//		L.terminate();
	}
	
	private void writeStatistics(String msg, List<String> sites) {
		if (sites == null || sites.isEmpty())
			return;
		L.out("\r\n" + msg + "\r\n");
		for (String site : sites) {
			L.out("\t\"" + site + "\",\r\n");
		}
		L.end();
	}
	
	/** 如果可以通过文件创建和覆盖打补丁, 就实现这个函数 */
	protected void createPatch() {
	}
	
	private boolean skip(String site) {
		return filterSites != null && !filterSites.contains(site);
	}
	
	/**
	 * @param file 需要打补丁的文件, 相对路径
	 */
	protected void createPatch(String file) {
		File f = new File(P.BASE_PATH + file);
		if (!f.exists()) {
			System.out.println("not exists: " + P.BASE_PATH + file);
			return;
		}
		File destFile = new File(P.BASE_PATH + "build\\patchs\\" + file);
		if (!destFile.exists()) {
			destFile.getParentFile().mkdirs();
		}
		copyFile(f, destFile);
	}
	
	protected String getLocalPath(String path) {
		if (path.matches("^[a-zA-Z]:\\.+"))
			return path;
		return P.BASE_PATH + path;
	}
	
	protected abstract void execute(String site, String url, int group, int index) throws IOException;
	
	public static StringBuilder read(InputStream in, String encode) {
		try {
			StringBuilder r = new StringBuilder();
			Reader reader = new InputStreamReader(in, encode);
			char[] buffer = new char[4096];
			int n;
			while ((n = reader.read(buffer)) > 0) {
				r.append(buffer, 0, n);
			}
			return r;
		} catch (Exception e) {
		}
		return null;
	}

	public static String readFile(String filePath) {
		File f = new File(filePath);
		try {
			return read(new FileInputStream(f), FILE_ENCODING).toString();
		} catch (IOException e) {
			return null;
		}
	}
	
	protected HttpClient getHttpClient() {
		DefaultHttpParams.getDefaultParams().setParameter("http.protocol.cookie-policy", CookiePolicy.BROWSER_COMPATIBILITY);
		HttpClient client = new HttpClient();
		HttpConnectionManagerParams p = client.getHttpConnectionManager().getParams();
		p.setConnectionTimeout(30000);
		p.setSoTimeout(60000);
		return client;
	}
	
	protected PostMethod getPostMethod(String url, List<NameValuePair> params) {
		PostMethod post = new PostMethod(url);
		post.getParams().setParameter(HttpClientParams.HTTP_CONTENT_CHARSET, REQUEST_ENCODING);
		post.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new HttpMethodRetryHandler() {
			@Override
			public boolean retryMethod(HttpMethod method, IOException exception, int executionCount) {
				return !(exception instanceof ConnectException) && executionCount < 3;
			}
		});
		if (params != null)
			post.setRequestBody(params.toArray(new NameValuePair[params.size()]));
		return post;
	}
	
	private void post(int index, String site, HttpClient client, PostMethod post) throws IOException {
		try {
			int statusCode = client.executeMethod(post);
			if (statusCode != HttpStatus.SC_OK) {
				L.result(index, "HTTP-" + statusCode);
				return;
			}
			String resp = getResponse(post);
			if (resultFilter == null || resultFilter.show(site, resp))
				L.result(index, resp);
		} catch (IOException e) {
			L.result(index, e.getClass().getSimpleName() + ": " + e.getMessage());
			throw e;
		}
		post.releaseConnection();
	}
	
	protected void shieldPost(int index, String site, HttpClient client, PostMethod post) throws IOException {
		try {
			totalCount ++;
			post(index, site, client, post);
		} catch (UnknownHostException e) {
			throw e;
		} catch (ConnectException e) {
			errorCount ++;
			if (errorCount >= 3 && totalCount == errorCount)
				throw e;
//		} catch (IOException e) {
//			throw e;
		} catch (Throwable e) {
			//block other exception
			e.printStackTrace();
		}
	}
	
	protected String getResponse(HttpMethodBase method, int max) throws IOException {
//		long len = method.getResponseContentLength();
		StringBuilder sb = new StringBuilder();
        char[] buffer = new char[1024];
        int n, t=0;
        InputStreamReader reader = new InputStreamReader(method.getResponseBodyAsStream(), method.getResponseCharSet());
        while ((n = reader.read(buffer)) > 0) {
        	sb.append(buffer, 0, n);
			t += n;
			if (max > 0 && t >= max)
				break;
        }
        reader.close();
        method.releaseConnection();
        return sb.toString();
	}
	
	private String getResponse(HttpMethodBase method) throws IOException {
		return getResponse(method, 256);
	}
	
	public void setPatchOnly(boolean patchOnly) {
		this.patchOnly = patchOnly;
	}
	
	private static void copyFile(File source, File dest) {
		try {
			FileInputStream fis = new FileInputStream(source);
			FileOutputStream fos = new FileOutputStream(dest);
			byte[] buff = new byte[1024];
			int readed = -1;
			while ((readed = fis.read(buff)) > 0)
				fos.write(buff, 0, readed);
			fis.close();
			fos.close();
		} catch (IOException e) {
			System.out.println(e.getClass().getSimpleName() + ": " + e.getMessage());
		}
	}
	
	public static boolean handleConfigs(Map<String, String> configs, String config) {
		configs.clear();
		if (config.startsWith(CONFIG)) {
			String[] cs = config.substring(CONFIG.length()).split(",");
			for (int i = 0; i < cs.length; i++) {
				int p = cs[i].indexOf(':');
				if (p != -1) {
					configs.put(cs[i].substring(0, p), cs[i].substring(p + 1));
				} else {
					configs.put(cs[i], "");
				}
			}
			return true;
		}
		return false;
	}
	
	public static boolean isConfig(String str) {
		return str.startsWith(CONFIG);
	}
	
	private Log getLog() {
		if (L == null) {
			String fn = getFunction();
			L = new Log(Definition.LOG_PATH + fn + ".txt");
		}
		return L;
	}
	
	protected void onFinish(int group, int total) {
	}
	
	static String getFunction() {
		Throwable e = new Throwable();
		StackTraceElement[] es = e.getStackTrace();
		int line = -1;
		for (int i = 0; i < es.length; i++) {
			StackTraceElement s = es[i];
			if (Main.class.getName().equals(s.getClassName()) && "main".equals(s.getMethodName())) {
				line = s.getLineNumber();
				break;
			}
		}
		if (line == -1)
			return null;
		File f = new File("E:\\Workspaces\\php\\money\\ues\\jude\\java\\money\\batch\\Main.java");
		try {
			BufferedReader reader = new BufferedReader(new FileReader(f));
			String content = null;
			for (int i = 0; i<line; i++) {
				String c = reader.readLine();
				if (c == null)
					break;
				if (i == line - 1)
					content = c;
			}
			if (content == null)
				return null;
//			System.out.print(content);
			Pattern p = Pattern.compile("^\\s+run\\(Tasks\\.([^\\(]+)\\(\\)\\);$");
			Matcher m = p.matcher(content);
			if (m.find()) {
				return m.group(1);
			}
		} catch (Exception ex) {
			return ex.getClass().getSimpleName();
		}
		return null;
	}
	
}
